/* 
 * Copyright 2011 kOSMik.
 * This file is part of kOSMik.
 * 
 * kOSMik is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * kOSMik is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with kOSMik.  If not, see <http://www.gnu.org/licenses/>.
 *  
 * kOSMik:
 * Open Street Map - Editor.
 * http://code.google.com/p/swp-dv-ws2010-osm-2/
 */

package de.fuberlin.kosmik.trackrecorder;

import java.util.LinkedList;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;
import de.fuberlin.kosmik.R;
import de.fuberlin.kosmik.data.DataBaseConnector;
import de.fuberlin.kosmik.data.DataBaseConnectorImpl;
import de.fuberlin.kosmik.filter.SmoothingFilter;
import de.fuberlin.kosmik.filter.TrackFilter;
import de.fuberlin.kosmik.gui.activity.ViewActivity;
import de.fuberlin.kosmik.gui.view.ServiceLocationListener;
import de.fuberlin.kosmik.log.Constants;
import de.fuberlin.kosmik.settings.Settings;

/**
 * TrackRecorderService is a component between GUI and database. It implements
 * LocationListener to obtain new GPS fixes.
 * 
 * @author Micha & Simon
 */
public class TrackRecorderService extends Service implements LocationListener,
		OnSharedPreferenceChangeListener {
	/**
	 * Bundle Wrapper stores pointId of a point and its attributes in a bundle.
	 */
	private static class BundleWrapper {
		protected long pointId;
		protected Bundle bundle;

		/**
		 * @param pId pointId of the point
		 * @param bndl attributes of the point
		 */
		protected BundleWrapper(long pId, Bundle bndl) {
			pointId = pId;
			bundle = bndl;
		}
	}

	// static variables
	private static boolean serviceIsRunning;

	// reference to the singleton TrackRecorderService
	private static TrackRecorderService service;

	private static final int NOTIFICATION_ID = 1;
	private static final int MILLIS_TO_SEC = 1000;

	private static final String TAG = Constants.TRACK_RECORDING_SERVICE;

	/**
	 * Starts TrackRecorderService if necessary and returns the reference to the
	 * singleton. NOTE: reference is null, if TrackRecorderService is not yet
	 * running.
	 * 
	 * @param context parent context
	 * @return reference to the singleton
	 */
	public static TrackRecorderService getService(Context context) {
		if (!serviceIsRunning) {

			Log.d(TAG, "TrackRecorderService getService: service is null");
			startService(context);
		}
		return service;
	}

	/**
	 * Starts TrackRecorderService if necessary.
	 * 
	 * @param context parent context
	 */

	public static void startService(Context context) {

		if (!serviceIsRunning) {
			context.startService(new Intent(context, TrackRecorderService.class));
		}
	}

	/**
	 * Stops TrackRecorderService.
	 * 
	 * @param context context of caller
	 */
	public static void stopService(Context context) {

		if (service != null) {
			Log.d(TAG, "stopService");
			context.stopService(new Intent(context, TrackRecorderService.class));
			service = null;
		}
	}

	/**
	 * Returns whether service is running or not.
	 * 
	 * @return true if service is running, otherwise false.
	 */
	public static boolean isServiceRunning() {

		if (service != null) {
			return true;
		}
		return false;
	}

	private String ns = Context.NOTIFICATION_SERVICE;

	private NotificationManager notificationManager;
	private ServiceLocationListener locationListener;

	private boolean notifyListener;
	private LocationManager locationManager;

	private Settings settings;
	private SharedPreferences sharedPreferences;

	/**
	 * For database access.
	 */
	DataBaseConnector db;
	private float minDistance = 0.5F; // in meters

	private boolean gpsEnabled;
	private boolean isTracking;
	private boolean isPaused;
	private boolean useSmoothingFilter;
	private boolean useTrackingFilter;
	private int gpsInterval;

	private TrackFilter trackFilter;
	private SmoothingFilter locationFilter;

	private Location currentLocation;
	private long currentLocationId;
	private long trackId;
	private boolean trackingWay;
	private long wayId;
	private boolean trackingArea;
	private long areaId;

	// it shall be possible to add POIs right at the beginning,
	// when no location is available yet.

	private String currentTrackName;

	private LinkedList<Bundle> poiQueue = new LinkedList<Bundle>();

	/**
	 * Stops the current tracked area.
	 * 
	 * @param tags identifies a area, if we allow to track more then one area at
	 *            one time (not yet used)
	 */
	public void closeArea(Bundle tags) {
		if (trackingArea) {
			Log.d(TAG, "TRC: closeArea");
			if (notifyListener)
				locationListener.onStopArea();
			trackingArea = false;
		}
	}

	/**
	 * Gives the name of the current track.
	 * 
	 * @return name of the current track
	 */
	public String getCurrentTrackName() {
		return currentTrackName;
	}

	/**
	 * Returns the id of the current track.
	 * 
	 * @return trackId
	 */
	public long getCurrentTrackId() {
		return trackId;
	}

	/**
	 * Returns the id of the current point.
	 * 
	 * @return pointId
	 */
	public long getCurrentPointId() {
		return currentLocationId;
	}

	/**
	 * Checks whether the gps provider is enabled.
	 * 
	 * @return true if gps provider is enabled
	 */
	public boolean isGpsEnabled() {
		gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
		return gpsEnabled;
	}

	/**
	 * We don't use the android binder mechanism, so return null.
	 */
	@Override
	public IBinder onBind(Intent intent) {
		return null;
	}

	private static void setService(TrackRecorderService service) {
		TrackRecorderService.service = service;
	}

	private static void setServiceAsRunning() {
		TrackRecorderService.serviceIsRunning = true;
	}

	private static void setServiceAsStopped() {
		TrackRecorderService.serviceIsRunning = false;
	}

	/**
	 * Registers SettingsListener and LocationUpdates.
	 */
	@Override
	public void onCreate() {
		super.onCreate();
		setService(this);
		setServiceAsRunning();

		settings = new Settings(this);
		useSmoothingFilter = settings.useSmoothingFilter();
		useTrackingFilter = settings.useTrackFilter();
		gpsInterval = settings.gpsInterval();

		/* init filters before gps is enabled */
		trackFilter = new TrackFilter(10000, 15, 5);
		locationFilter = new SmoothingFilter();
		// get gps LocationManager
		locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
		isGpsEnabled();
		db = new DataBaseConnectorImpl(this);
		registerSettingsListener();
	}

	/**
	 * Unregisters SettingsListener and LocationUpdates.
	 */
	@Override
	public void onDestroy() {
		setServiceAsStopped();
		setService(null);
		unregisterSettingsListener();
		db.close();

		locationManager.removeUpdates(this);
		if (notificationManager != null)
			notificationManager.cancelAll();

	}

	/**
	 * Receives a new GPS fix and delegates it to the database wrapper. Adds the
	 * GPS fix to ways or areas when they are tracked actually.
	 */
	@Override
	public void onLocationChanged(Location location) {
		Log.d(TAG,
				"new gps fix lat->" + location.getLatitude() + " lng->"
						+ location.getLongitude());
		isTracking = true;
		if (!isPaused) {

			Location newLocation = new Location(location);

			if (useSmoothingFilter) {
				newLocation = locationFilter.filter(newLocation);
			}

			if (useTrackingFilter) {
				newLocation = trackFilter.filter(newLocation);
				if (newLocation == currentLocation) {
					return;
				}
			}

			currentLocation = newLocation;
			currentLocationId = db.addPoint(currentLocation.getLongitude(),
					currentLocation.getLatitude(), System.currentTimeMillis(), trackId);

			if (notifyListener) {
				locationListener.onLocationUpdate(currentLocation.getLongitude(),
						currentLocation.getLatitude());
			}

			if (trackingWay) {
				final AsyncTask<Long, Integer, Long> asyncTask = new AsyncTask<Long, Integer, Long>() {
					@Override
					protected Long doInBackground(Long... ids) {
						db.addPointToWay(ids[0].longValue(), ids[1].longValue());
						Log.d(Constants.TRACK_RECORDING_SERVICE, " added point to way");
						return null;
					}
				};
				asyncTask.execute(Long.valueOf(currentLocationId), Long.valueOf(wayId));
			}
			if (trackingArea) {
				final AsyncTask<Long, Integer, Long> asyncTask = new AsyncTask<Long, Integer, Long>() {
					@Override
					protected Long doInBackground(final Long... ids) {
						db.addPointToWay(ids[0].longValue(), ids[1].longValue());
						Log.d(Constants.TRACK_RECORDING_SERVICE, " added point to area");
						return null;
					}
				};
				asyncTask.execute(Long.valueOf(currentLocationId), Long.valueOf(areaId));
			}

			if (!poiQueue.isEmpty()) {
				Log.d(TAG, "TRC: Added Poi from Queue");
				for (Bundle b : poiQueue) {
					onPOI(b);
				}
				poiQueue.clear();
			}
		}
	}

	/**
	 * Adds a new POI with the given attributes.
	 * 
	 * @param tags contains the attributes of the POI
	 */
	public void onPOI(Bundle tags) {

		if (null != currentLocation) {
			final AsyncTask<BundleWrapper, Integer, Long> asyncTask = new AsyncTask<BundleWrapper, Integer, Long>() {
				@Override
				protected Long doInBackground(BundleWrapper... bundleWrp) {
					Log.d(Constants.TRACK_RECORDING_SERVICE, "TRC: onPoi currentLocationId->"
							+ bundleWrp[0].pointId + " tags ->" + bundleWrp[0].bundle);

					db.addPoi(bundleWrp[0].pointId, bundleWrp[0].bundle);

					return null;
				}
			};
			asyncTask.execute(new BundleWrapper(currentLocationId, tags));

			// TODO: tag name
			String tag = tags.containsKey("KEY") ? tags.getString("KEY") : "";
			if (notifyListener) {
				locationListener.onPOI(currentLocation.getLongitude(),
						currentLocation.getLatitude(), tag);
			}
		} else {
			Log.d(Constants.TRACK_RECORDING_SERVICE, "TRC: Adding Poi to Queue");
			poiQueue.add(tags);
		}
	}

	/**
	 * Is called when the user disables the gps provider.
	 */
	@Override
	public void onProviderDisabled(String provider) {
		gpsEnabled = false;
		Log.d(TAG, "gps provider disabled");
	}

	/**
	 * Is called when the user enables the gps provider.
	 */
	@Override
	public void onProviderEnabled(String provider) {
		gpsEnabled = true;
		Log.d(TAG, "gps provider enabled");

	}

	/**
	 * Starts the service sticky, so it is running until it is explicitly
	 * stopped.
	 */
	// @Override
	// public int onStartCommand(Intent intent, int flags, int startId) {
	// Log.d(Constants.TAG, "Received start id " + startId + ": " + intent);
	// // We want this service to continue running until it is explicitly
	// // stopped, so return sticky.
	// return START_STICKY;
	// }

	/**
	 * Start the service. Called by the system.
	 */
	@Override
	public void onStart(Intent intent, int startId) {
		Log.d(TAG, "Received start id " + startId + ": " + intent);
	}

	/**
	 * Pauses the current track. The LocationManager is still running to avoid a
	 * GPS fix delay.
	 */
	public void pauseTrack() {
		if (!isPaused) {
			isTracking = false;
			unregisterLocationListener();
			showNotificationPaused();
			isPaused = true;
		}
	}

	/**
	 * Registers location listener to obtain GPS fixes.
	 */
	private void registerLocationListener() {

		// DEBUG
		// locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
		// settings.gpsInterval() * MILLIS_TO_SEC, minDistance, this);

		locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, gpsInterval
				* MILLIS_TO_SEC, minDistance, this);

	}

	/**
	 * Enables notifications for the locationListener. The locationListener will
	 * be informed if the location changes or new points are added.
	 * 
	 * @param locListener which wants to be informed about location changes
	 */
	public void registerLocationUpdates(ServiceLocationListener locListener) {
		Log.d(TAG, "TRS: registerLocationUpdates ");
		this.locationListener = locListener;
		notifyListener = true;
	}

	/**
	 * Registers SettingsListener to obtain setting changes i.e GPS interval.
	 */
	private void registerSettingsListener() {
		sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
		sharedPreferences.registerOnSharedPreferenceChangeListener(this);
	}

	@Override
	public void onSharedPreferenceChanged(SharedPreferences pref, String arg) {
		Log.d(Constants.SETTINGS, "setting changed");
		if ((TrackRecorderService.this).getResources()
				.getString(R.string.settings_key_gps_interval).equals(arg)) {
			Log.d(Constants.SETTINGS, "gps interval changed");
			// pause and resume Track to change gps interval of the
			// location locationListener
			gpsInterval = settings.gpsInterval();
			TrackRecorderService.this.restartTracking();
		} else if ((TrackRecorderService.this).getResources()
				.getString(R.string.settings_key_osm_layer).equals(arg)) {
			Log.d(Constants.SETTINGS, "showOSMlayer changed");
		} else if ((TrackRecorderService.this).getResources()
				.getString(R.string.settings_key_smooth_filter).equals(arg)) {
			Log.d(Constants.SETTINGS, "smoothing filter pref changed");
			useSmoothingFilter = settings.useSmoothingFilter();
		} else if ((TrackRecorderService.this).getResources()
				.getString(R.string.settings_key_filter_btn).equals(arg)) {
			Log.d(Constants.SETTINGS, "tracking filter pref changed");
			useTrackingFilter = settings.useTrackFilter();
		}
	}

	/**
	 * Pauses and resumes tracking to update preferences of the LocationListener
	 * i.e. GPS interval
	 */
	void restartTracking() {
		if (!isPaused) {
			pauseTrack();
			resumeTrack();
		}
	}

	/**
	 * Resumes the paused track.
	 */

	public void resumeTrack() {
		if (isPaused) {
			registerLocationListener();
			showNotificationRunning();
			isPaused = false;
		}
	}

	/**
	 * Shows pause notification icon in statusbar.
	 */
	private void showNotificationPaused() {
		if (notificationManager == null)
			return;
		Log.w(TAG, "no notification manager, so can't notify track is paused");
		// TODO: modify notification paused
		// Instantiate the Notification:
		int icon = R.drawable.ic_stat_notify_paused;
		CharSequence tickerText = this.getString(R.string.service_notification_paused);
		long when = System.currentTimeMillis();

		Notification notification = new Notification(icon, tickerText, when);

		// Define the Notification's expanded message and Intent:
		Context context = getApplicationContext();
		CharSequence contentTitle = this.getString(R.string.app_name);
		CharSequence contentText = this.getString(R.string.service_notification_paused);

		Intent notificationIntent = new Intent(this, ViewActivity.class);
		PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);

		notification.setLatestEventInfo(context, contentTitle, contentText, contentIntent);

		// Pass the Notification to the NotificationManager:

		notificationManager.notify(NOTIFICATION_ID, notification);

	}

	/**
	 * Shows running-notification icon in statusbar.
	 */
	private void showNotificationRunning() {
		// Instantiate the Notification:
		if (notificationManager == null) {
			Log.w(TAG, "no notification listener, so could not notifiy track is started");
			return;
		}
		int icon = R.drawable.ic_stat_notify_running;
		CharSequence tickerText = this.getString(R.string.service_notification_running);
		long when = System.currentTimeMillis();

		Notification notification = new Notification(icon, tickerText, when);

		// Define the Notification's expanded message and Intent:
		Context context = getApplicationContext();
		CharSequence contentTitle = this.getString(R.string.app_name);
		CharSequence contentText = this.getString(R.string.service_notification_running);

		Intent notificationIntent = new Intent(this, ViewActivity.class);
		notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
				| Intent.FLAG_ACTIVITY_SINGLE_TOP);
		PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);

		notification.setLatestEventInfo(context, contentTitle, contentText, contentIntent);

		// Pass the Notification to the NotificationManager:

		notificationManager.notify(NOTIFICATION_ID, notification);
	}

	/**
	 * Starts a new area with the given attributes.
	 * 
	 * @param tags contains the attributes of the area
	 */
	public void startArea(Bundle tags) {
		if (!trackingArea) {
			Log.d(Constants.TAG, "TRC: startArea");
			areaId = db.startWay(tags, true);
			if (notifyListener) {
				locationListener.onStartArea();
			}
			trackingArea = true;
		}
	}

	/**
	 * Starts a new track. The LocationManger is started immediately to obtain
	 * GPS fixes.
	 * 
	 * @param trackName name of the new track ( key:"TRACK_NAME")
	 */
	public void startTrack(String trackName) {

		Log.d(TAG, "TRS: startTrack -> " + trackName);

		registerLocationListener();

		Log.d(TAG, "TRS: gpsEnabled->" + gpsEnabled);

		trackId = db.addTrack(trackName, System.currentTimeMillis());
		this.currentTrackName = trackName;
		// Get a reference to the NotificationManager:
		notificationManager = (NotificationManager) getSystemService(ns);
		showNotificationRunning();

	}

	/**
	 * Starts a new way with the given attributes.
	 * 
	 * @param tags contains the attributes of the way
	 */
	public void startWay(Bundle tags) {

		if (!trackingWay) {
			Log.d(TAG, "TRC: startWay");
			wayId = db.startWay(tags, false);
			if (notifyListener) {
				locationListener.onStartWay();
			}
			trackingWay = true;
		}
	}

	/**
	 * Stops current track and the LocationManager.
	 */
	public void stopTrack() {
		unregisterLocationListener();
		isTracking = false;
		if (notificationManager != null) {
			notificationManager.cancelAll();
		} else {
			Log.w(TAG, "notificationManager was null, could not cancel it");
		}
	}

	/**
	 * Stops the current tracked way.
	 * 
	 * @param tags identifies a way, if we allow to track more then one way at
	 *            one time (not yet used)
	 */
	public void stopWay(Bundle tags) {
		if (trackingWay) {
			Log.d(TAG, "TRC: stopWay");
			if (notifyListener) {
				locationListener.onStopWay();
			}
			trackingWay = false;
		}
	}

	/**
	 * Unregisters location listener, stops obtaining GPS fixes.
	 */
	private void unregisterLocationListener() {
		locationManager.removeUpdates(this);
	}

	/**
	 * Disables ServiceLocationListener's notifications.
	 */
	public void unregisterLocationUpdates() {
		notifyListener = false;
		this.locationListener = null;
	}

	/**
	 * Unregisters SettingsListener, stops obtaining setting changes.
	 */
	private void unregisterSettingsListener() {
		sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
	}

	/**
	 * Informs about the the state of tracking.
	 * 
	 * @return true if the service has a actual gps fix
	 */
	public boolean isTracking() {
		return isTracking;
	}

	@Override
	public void onStatusChanged(String provider, int status, Bundle extras) {
		// TODO: NOTIFY USER?
		if (provider.equals(LocationManager.GPS_PROVIDER)) {
			switch (status) {
			case LocationProvider.AVAILABLE:
				gpsEnabled = true;
				break;

			case LocationProvider.OUT_OF_SERVICE:
				gpsEnabled = false;
				break;

			case LocationProvider.TEMPORARILY_UNAVAILABLE:
				gpsEnabled = false;
				break;

			default:
				// nothing...
			}
		}
	}
}
